#!/usr/bin/env python

"""
Server
Copyright (C) 2007 Andrew Briscoe

A the server for PIGS.
"""

__license__ = 'GPL http://www.gnu.org/licenses/gpl.txt'
__author__ = 'Andrew Briscoe <everynothing@gmail.com>'
__version__ = '0.0.1'
__program__ = 'PIGS'

from twisted.internet import protocol, reactor
from twisted.protocols import basic
from random import Random
import Game, Display, RandomStuff

REQUEST_LOGIN = 'Welcome to PIGS, please enter your handle.'
SEPARATE, MINI_SEPARATE = '%^#@!', '{|3.'

def rand_num(max_num):
    "Returns a random number from 0 to max, +1."
    
    return int(Random().random() * max_num) + 1

def grab_random_number(list_of_nums):
    "Grabs a random number not in list."
    
    number = None
    while not number or number in list_of_nums:
        number = rand_num(1000)
    return number

def msg_people(people, array):
    "Send a message to a bunch of people."
    
    for person in people:
        person.message(array)
        
class ChatRoom(object):
    "Represents a chat room."
    
    def __init__(self, server, name, description='', creator=None):
        
        self.server, self.name, self.description = server, name, description
        self.str = MINI_SEPARATE.join([self.name, self.description])
        self.is_game_chat = self.name.split(' ')[0] == 'Game!->'
        self.occupants = []
        self.server.chat_rooms += [self]
        self.add_occupant(creator)
        self.update_chats()
        
    def update_chats(self):
        "Notifies the server that seeks were updated."
        
        if not self.is_game_chat:
            self.server.channels_updated()
    
    def add_occupant(self, joiner):
        "Joiner has joined the room."
        
        if joiner and joiner not in self.occupants:
            self.occupants += [joiner]
            joiner.chats += [self]
            if not self.is_game_chat:
                joiner.message(['joined room', self.name, self.description])
            self.occupants_updated()
        
    def remove_chat(self):
        "Removes this chat from the server."
        
        if self in self.server.chat_rooms:
            for occupant in self.occupants:
                occupant.chats.remove(self)
            self.server.chat_rooms.remove(self)
            self.update_chats()
        
    def remove_occupant(self, leaver):
        "Leaver has left the room."
        
        if leaver in self.occupants:
            self.occupants.remove(leaver)
            leaver.chats.remove(self)
            if not self.occupants and self.name != 'Main':
                self.remove_chat()
            else:
                self.occupants_updated()
            
    def send_message(self, handle, text):
        "Player sends text to the chat room."
        
        to_send = handle + ": " + text
        msg_people(self.occupants, ['chat', self.name, to_send])
        
    def occupants_updated(self):
        "Occupants has been updated, tell players."
        
        msg_people(self.occupants, 
                   ['players', self.name] + self.player_strings())
        
    def player_strings(self):
        "A string of all players in this chat."
        
        return [o.handle for o in self.occupants]
    
class Seek(object):
    "Represents a seek."
    
    def __init__(self, server, creator, game, description):
        
        num = grab_random_number([s.number for s in server.seeks])
        self.server, self.creator, self.number, self.game, \
            self.description = server, creator, num, game, description
        self.str = MINI_SEPARATE.join([str(self.number), 
               self.creator.handle] + str_array(self.game) + [self.description])
        same_seek = server.seek_by_seekstr(str_array(self.game))
        if same_seek:
            self.start_game(same_seek.creator)
        else:
            creator.seeks += [self]
            server.seeks += [self]
            server.seeks_updated()
            
    def start_game(self, opponent):
        "player tries to start the game."
        
        if not opponent == self.creator and not opponent.game:
            ServerGame(self.server, self.game, [self.creator, opponent])
            for pla in [self.creator, opponent]:
                pla.destroy_seeks()
        
    def destroy_seek(self):
        "Tells everyone this seek is dead."
        
        self.creator.seeks.remove(self)
        self.server.seeks.remove(self)
        self.server.seeks_updated()
        
def str_array(gam):
    "Make game into a string array."
    
    return [gam.game_type, gam.board_name, gam.variant_type]
    
class ServerGame(object):
    "Represents a game."
    
    def __init__(self, server, game, players):
        
        num = grab_random_number([s.number for s in server.games])
        self.server, self.number, self.players, self.game, self.requests = \
            server, num, players, game, [None for p in players]
        self.str = MINI_SEPARATE.join([str(self.number)] + \
              [p.handle for p in self.players] + str_array(self.game))
        self.current_player = self.players[game.board.turn]
        self.chat = ChatRoom(server, 'Game!-> ' + str(self.number), '')
        cnt = 0
        for player in players:
            player.game, player.player = self, cnt
            self.msg_join(player)
            cnt += 1
        self.server.games += [self]
        self.server.games_updated()
        
    def msg_join(self, person, obs=False):
        "Sends a join message."
        
        player = 99 if obs else person.player
        person.message(['started game', str(self.number)] + \
           str_array(self.game) + [player] + [p.handle for p in self.players])
        self.chat.add_occupant(person)
        
    def add_observer(self, observer):
        "Add observer to this game."
        
        if not observer.handle in [p.handle for p in self.everyone()]:
            self.msg_join(observer, True)
            for action in self.game.get_action_stack():
                observer.message(["act", self.number, str(action)])
        
    def try_action(self, trying_player, action_string):
        "trying_player attempts action."
        
        gam = self.game
        action = Game.action_from_string(action_string, gam.board)
        if action and gam.action_legal(action) and \
            trying_player in self.players and \
            (not action.ONLY_CURRENT or trying_player == self.current_player):
            self.game.execute_action(action)
            msg_people(self.everyone(), ["act", self.number, action_string])
            self.current_player = self.players[self.game.turn()]
            self.requests = [None for p in self.players]
            if self.game.game_over():
                self.end_game()
                
    def player_left(self, player):
        "What happens when a player leaves prematurely."
        
        for cnt in xrange(len(self.players)):
            if player == self.players[cnt]:
                self.try_action(player, str(Game.Resign(cnt)))
                self.end_game()
                
    def end_game(self):
        "Game was ened for the reason in text."
        
        if self in self.server.games:
            msg = self.game.message() if self.game.game_over() \
                else "Player left, game aborted."
            msg_people(self.everyone(), [msg])
            for player in self.players:
                player.game = None
            self.server.games.remove(self)
            self.server.games_updated()
    
    def kibitz(self, handle, message):
        "Person tries to kibitz.  Right now allows anyone."
        
        self.chat.send_message(handle, message)
        
    def everyone(self):
        "Iterates over all players and observers of this game."
        
        return self.chat.occupants
    
    def request(self, player, request):
        "Player makes a request."
        
        requester = self.players[player]
        nxt_player = self.game.board._next_player(player)
        if self.requests[nxt_player] == request:
            req = 'Draw' if request == 'draw request' else \
                'Take Back' if request == 'undo request' else ''
            self.try_action(requester, req)
        else:
            self.requests[player] = request
            self.players[nxt_player].message(['request', request])
    
class PIGSServer(protocol.ServerFactory):
    "Class the represents the overall state of the server."
    
    def __init__(self):
        "Starts up the server."
        
        print 'Server started!'
        self.protocol, self.players, self.games, self.seeks, self.chat_rooms = \
            Player, [], [], [], []
        ChatRoom(self, 'Main', 'Talk about any/everything!')
        
    def player_left(self, leaver):
        "What happens when a player dies."
        
        if leaver.handle:
            if leaver.game:
                leaver.game.player_left(leaver)
            while leaver.chats:
                leaver.chats[0].remove_occupant(leaver)
            leaver.destroy_seeks()
            self.players.remove(leaver)
        
    def message_all(self, array):
        "Sends a message to all players."
        
        msg_people(self.players, array)
            
    def is_handle_okay(self, handle):
        "Is handle valid?"
        
        leng = len(handle)
        return leng > 3 and leng < 13 and not self.player_by_handle(handle)
            
    def seek_strings(self):
        "Return all the seek strings, except for player."
        
        return [s.str for s in self.seeks]
        
    def channel_strings(self):
        "Return all the seek strings, except for player."
        
        return [c.str for c in self.chat_rooms if not c.is_game_chat]
    
    def game_strings(self):
        "Return all the seek strings, except for player."
        
        return [g.str for g in self.games]
    
    def seek_by_number(self, number):
        "Return a seek by its number."
        
        for seek in self.seeks:
            if seek.number == number:
                return seek
            
    def game_by_number(self, number):
        "Return a seek by its number."
        
        for game in self.games:
            if game.number == number:
                return game
            
    def chat_by_name(self, name):
        "Return a seek by its number."
        
        for chat in self.chat_rooms:
            if chat.name == name:
                return chat
            
    def player_by_handle(self, name):
        "Return a seek by its number."
        
        for person in self.players:
            if person.handle == name:
                return person
            
    def seek_by_seekstr(self, seek_str):
        "Returns a seek thats game string is equal to seek_str."
        
        for seek in self.seeks:
            if str_array(seek.game) == seek_str:
                return seek
            
    def send_to_all(self, array):
        "Send array to everyone."
        
        msg_people(self.players, array)
            
    def seeks_updated(self):
        "Seeks have been updated, tell everyone."
        
        seeks = self.seek_strings()
        self.send_to_all(['seeks'] + seeks)
         
    def games_updated(self):
        "Games have been updated, tell everyone."
        
        games = self.game_strings()
        self.send_to_all(['games'] + games)
        
    def channels_updated(self):
        "Channels have been updated, tell everyone."
        
        channels = self.channel_strings()
        self.send_to_all(['channels'] + channels)
    
class Player(basic.LineReceiver):
    "The protocol PIGS uses.  Pretty much telnet."
    
    def connectionMade(self):
        "What happens when a client connects to this server."
        
        self.server, self.handle, self.game, self.seeks, self.chats, self.player = \
            self.factory, None, None, [], [], 0
        self.message([REQUEST_LOGIN])
            
    def __eq__(self, other):
        "Is this the same player as other?"
        
        return other and self.handle == other.handle
    
    def connectionLost(self, reason):
        "When a player loses connection remove them from the server."
        
        self.server.player_left(self)

    def lineReceived(self, line):
        "What to do when the user sends a command."
        
        tok = line.split(SEPARATE)
        typ, mess = tok[0], lambda x: self.message(x)
        print self.handle, tok
        
        if not self.handle:
            if self.server.is_handle_okay(line):
                self.server.players += [self]
                self.handle = line
                mess(['good login', line])
                self.message(['channels'] + self.server.channel_strings())
                self.message(['games'] + self.server.game_strings())
                self.message(['seeks'] + self.server.seek_strings())
                self.server.chat_rooms[0].add_occupant(self)
            else:
                mess([REQUEST_LOGIN])
        elif typ == 'act':
            self.try_action(tok[1])
        elif typ == 'resign':
            self.try_action('Resign ' + str(self.player))
        elif typ == 'draw request' or typ == 'undo request':
            if self.game:
                self.game.request(self.player, typ)
        elif typ in ['chat', 'join channel', 'leave chat']:
            channel = self.server.chat_by_name(tok[1])
            if channel:
                if typ == 'chat':
                    channel.send_message(self.handle, tok[2])
                elif typ == 'join channel':
                    channel.add_occupant(self)
                elif typ == 'leave chat':
                    channel.remove_occupant(self)
        elif typ == 'seek':
            Seek(self.server, self, 
                 RandomStuff.game_from_strings(tok[1], tok[2], tok[3]), tok[4])
        elif typ == 'accept seek':
            seek = self.server.seek_by_number(int(tok[1]))
            if seek:
                seek.start_game(self)
        elif typ == 'join game':
            game = self.server.game_by_number(int(tok[1]))
            if game:
                game.add_observer(self)
        elif typ == 'create':
            if not self.server.chat_by_name(tok[1]):
                ChatRoom(self.server, tok[1], tok[2], self)
        elif typ == 'logout':
            self.connectionLost('')
    
    def message(self, array):
        "Joins the array (making all components strings) and then messages it!"
        
        self.sendLine(SEPARATE.join([str(t) for t in array]))
        
    def destroy_seeks(self):
        "Destroys all seeks this player has."
        
        while self.seeks:
            self.seeks[0].destroy_seek()
            
    def try_action(self, action_string):
        "If has a game tries actionstring."
        
        if self.game:
            self.game.try_action(self, action_string)

if __name__ == '__main__':
    RandomStuff.server_start(reactor, PIGSServer())
